package com.tianji.promotion.service;


import com.tianji.api.dto.promotion.CouponDiscountDTO;
import com.tianji.api.dto.promotion.OrderCouponDTO;
import com.tianji.api.dto.promotion.OrderCourseDTO;
import com.tianji.common.utils.CollUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.enums.UserCouponStatus;
import com.tianji.promotion.mapper.UserCouponMapper;
import com.tianji.promotion.strategy.discount.Discount;
import com.tianji.promotion.strategy.discount.DiscountStrategy;
import com.tianji.promotion.utils.PermuteUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author : Cammy.Wu
 * Description : 
 */

@Service
@Slf4j
@RequiredArgsConstructor
public class DiscountServiceImpl implements DiscountService {


    private final UserCouponMapper userCouponMapper;

    private final CouponScopeService scopeService;

    private final Executor discountSolutionExecutor;

    /**
     * 查询我的优惠券可用方案
     * @param orderCourses
     * @return
     */
    @Override
    public List<CouponDiscountDTO> findDiscountSolution(List<OrderCourseDTO> orderCourses) {
        // 1.查询我的所有可用优惠券
        List<Coupon> coupons = userCouponMapper.queryMyCoupons(UserContext.getUser());
        if (CollUtils.isEmpty(coupons)) {
            return CollUtils.emptyList();
        }
        // 2.初筛
        // 2.1.计算订单总价
        int totalAmount = orderCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();
        List<Coupon> availableCoupons = coupons.stream()
                .filter(c -> DiscountStrategy.getDiscount(c.getDiscountType()).canUse(totalAmount, c))
                .collect(Collectors.toList());
        if (CollUtils.isEmpty(availableCoupons)) {
            return CollUtils.emptyList();
        }

        // 3.排列组合出所有方案
        // 3.1.细筛（找出每一个优惠券的可用的课程，判断课程总价是否达到优惠券的使用需求）
        Map<Coupon, List<OrderCourseDTO>> availableCoupon = findAvailableCoupon(availableCoupons, orderCourses);
        if (CollUtils.isEmpty(availableCoupon)) {
            return CollUtils.emptyList();
        }
        // 3.2.排列组合
        availableCoupons = new ArrayList<>(availableCoupon.keySet());
        List<List<Coupon>> solutions = PermuteUtil.permute(availableCoupons);
        // 3.3 添加单券的方案
        for (Coupon coupon : availableCoupons) {
            solutions.add(List.of(coupon));
        }
        // 4.计算方案的优惠明细
        List<CouponDiscountDTO> list = Collections.synchronizedList(new ArrayList<>(solutions.size()));
//        for (List<Coupon> solution : solutions) {
//            list.add(calculateSolutionDiscount(availableCoupon, orderCourses, solution));
//        }
        // 4.1 定义闭锁
        CountDownLatch latch = new CountDownLatch(solutions.size());
        for (List<Coupon> solution : solutions) {
            // 4.2 异步计算
            CompletableFuture
                    .supplyAsync(
                            () -> calculateSolutionDiscount(availableCoupon, orderCourses, solution),
                            discountSolutionExecutor
                    ).thenAccept(couponDiscountDTO -> {
                        // 4.3 提交任务结果
                        list.add(couponDiscountDTO);
                        latch.countDown();
                    });
        }
        // 4.4 等待运算结束
        try {
            latch.await(1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            log.error("优惠方案计算被中断，{}", e.getMessage());
        }

        // 5.筛选最优解
        return findBestSolution(list);
    }


    /**
     * 查找可用的优惠券及其对应的可用课程列表
     *
     * @param coupons 优惠券列表
     * @param courses 课程列表
     * @return 一个映射，键为可用的优惠券，值为该优惠券对应的可用课程列表
     */
    private Map<Coupon, List<OrderCourseDTO>> findAvailableCoupon(
            List<Coupon> coupons, List<OrderCourseDTO> courses) {
        // 初始化一个 HashMap 用于存储可用的优惠券及其对应的可用课程列表，初始容量为优惠券列表的大小
        HashMap<Coupon, List<OrderCourseDTO>> map = new HashMap<>(coupons.size());
        // 遍历所有优惠券
        for (Coupon coupon : coupons) {
            // 1.找出优惠券的可用课程
            // 初始假设所有课程都是该优惠券的可用课程
            List<OrderCourseDTO> availableCourses = courses;
            if (coupon.getSpecific()) {
                // 1.1 限定了范围，查询券的可用范围
                // 使用 scopeService 查询该优惠券对应的所有可用范围
                List<CouponScope> scopes = scopeService.lambdaQuery().eq(CouponScope::getCouponId, coupon.getId()).list();
                // 1.2 获取范围对应的分类id
                // 从可用范围列表中提取出所有的分类 ID
                List<Long> scopeIds = scopes.stream().map(CouponScope::getBizId).collect(Collectors.toList());
                // 1.3 筛选课程
                // 根据分类 ID 筛选出可用的课程
                availableCourses = courses.stream()
                        .filter(c -> scopeIds.contains(c.getCateId())).collect(Collectors.toList());
            }
            // 检查是否有可用课程
            if (CollUtils.isEmpty(availableCourses)) {
                // 没有任何可用课程，跳过该优惠券
                continue;
            }
            // 2.计算课程总价
            // 计算可用课程的总价
            int totalNum = availableCourses.stream().mapToInt(OrderCourseDTO::getPrice).sum();
            // 3.判断是否可用
            // 根据优惠券的折扣类型获取对应的折扣策略
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            // 判断该优惠券在当前课程总价下是否可用
            if (discount.canUse(totalNum, coupon)) {
                // 如果可用，将优惠券及其对应的可用课程列表存入映射中
                map.put(coupon, availableCourses);
            }
        }
        // 返回存储可用优惠券及其对应可用课程列表的映射
        return map;
    }


    /**
     * 计算优惠券方案的折扣信息
     *
     * @param couponMap  优惠券与可用课程的映射，键为优惠券，值为该优惠券适用的课程列表
     * @param courses    所有课程的列表
     * @param solution   优惠券组合方案，即本次要使用的优惠券列表
     * @return 包含优惠券折扣信息的 DTO 对象
     */
    private CouponDiscountDTO calculateSolutionDiscount(
            Map<Coupon, List<OrderCourseDTO>> couponMap, List<OrderCourseDTO> courses, List<Coupon> solution) {
        // 1.初始化DTO
        // 创建一个新的 CouponDiscountDTO 对象，用于存储最终的折扣信息
        CouponDiscountDTO dto = new CouponDiscountDTO();
        // 2.初始化折扣明细的映射
        // 为每个课程初始化一个初始折扣为 0 的映射，键为课程 ID，值为该课程的折扣金额
        Map<Long, Integer> detailMap = courses.stream().collect(Collectors.toMap(OrderCourseDTO::getId, oc -> 0));
        // 3.计算折扣
        // 遍历优惠券组合方案中的每个优惠券
        for (Coupon coupon : solution) {
            // 3.1.获取优惠券限定范围对应的课程
            // 从优惠券与课程的映射中获取当前优惠券适用的课程列表
            List<OrderCourseDTO> availableCourses = couponMap.get(coupon);
            // 3.2.计算课程总价(课程原价 - 折扣明细)
            // 计算当前优惠券适用课程的总价，考虑之前已经计算过的折扣
            int totalAmount = availableCourses.stream()
                    .mapToInt(oc -> oc.getPrice() - detailMap.get(oc.getId())).sum();
            // 3.3.判断是否可用
            // 根据优惠券的折扣类型获取对应的折扣策略
            Discount discount = DiscountStrategy.getDiscount(coupon.getDiscountType());
            // 检查当前优惠券在当前课程总价下是否可用
            if (!discount.canUse(totalAmount, coupon)) {
                // 券不可用，跳过该优惠券，继续处理下一个
                continue;
            }
            // 3.4.计算优惠金额
            // 使用折扣策略计算当前优惠券的优惠金额
            int discountAmount = discount.calculateDiscount(totalAmount, coupon);
            // 3.5.计算优惠明细
            // 调用辅助方法计算每个课程的具体折扣金额
            calculateDiscountDetails(detailMap, availableCourses, totalAmount, discountAmount);
            // 3.6.更新DTO数据
            // 将当前优惠券的创建者 ID 添加到 DTO 中
            dto.getIds().add(coupon.getCreater());
            // 将当前优惠券的折扣规则添加到 DTO 中
            dto.getRules().add(discount.getRule(coupon));
            // 更新 DTO 中的总折扣金额
            dto.setDiscountAmount(discountAmount + dto.getDiscountAmount());
        }
        // 返回包含所有折扣信息的 DTO 对象
        return dto;
    }

    /**
     * 计算每个课程的具体折扣金额
     *
     * @param detailMap        课程 ID 与折扣金额的映射，用于存储每个课程的折扣信息
     * @param availableCourses 当前优惠券适用的课程列表
     * @param totalAmount      当前优惠券适用课程的总价
     * @param discountAmount   当前优惠券的总折扣金额
     */
    private void calculateDiscountDetails(Map<Long, Integer> detailMap, List<OrderCourseDTO> availableCourses, int totalAmount, int discountAmount) {
        // 记录当前处理的课程数量
        int times = 0;
        // 剩余未分配的折扣金额
        int remainDiscount = discountAmount;
        // 遍历当前优惠券适用的课程列表
        for (OrderCourseDTO course : availableCourses) {
            // 更新课程已计算数量
            times++;
            // 初始化当前课程的折扣金额
            int discount = 0;
            // 判断是否是最后一个课程
            if (times == availableCourses.size()) {
                // 是最后一个课程，总折扣金额 - 之前所有商品的折扣金额之和
                discount = remainDiscount;
            } else {
                // 计算折扣明细（课程价格在总价中占的比例，乘以总的折扣）
                discount = discountAmount * course.getPrice() / totalAmount;
                // 减去当前课程的折扣金额，更新剩余未分配的折扣金额
                remainDiscount -= discount;
            }
            // 更新折扣明细
            // 将当前课程的折扣金额累加到之前的折扣金额上
            detailMap.put(course.getId(), discount + detailMap.get(course.getId()));
        }
    }

    /**
     * 从给定的优惠券折扣方案列表中找出最优解
     *
     * @param list 优惠券折扣方案列表，包含多个可能的优惠券使用方案
     * @return 筛选出的最优优惠券折扣方案列表，按优惠金额降序排列
     */
    private List<CouponDiscountDTO> findBestSolution(List<CouponDiscountDTO> list) {
        // 1.准备Map记录最优解
        // moreDiscountMap 用于记录相同用券组合下，优惠金额最大的方案
        // 键为用券组合的字符串表示，值为对应的最优优惠券折扣方案
        Map<String, CouponDiscountDTO> moreDiscountMap = new HashMap<>();
        // lessCouponMap 用于记录相同优惠金额下，用券数量最少的方案
        // 键为优惠金额，值为对应的最优优惠券折扣方案
        Map<Integer, CouponDiscountDTO> lessCouponMap = new HashMap<>();
        // 2.遍历，筛选最优解
        // 遍历所有可能的优惠券使用方案
        for (CouponDiscountDTO solution : list) {
            // 2.1.计算当前方案的id组合
            // 将当前方案中使用的优惠券id排序并拼接成字符串，作为用券组合的唯一标识
            String ids = solution.getIds().stream()
                    .sorted(Long::compare).map(String::valueOf).collect(Collectors.joining(","));
            // 2.2.比较用券相同时，优惠金额是否最大
            // 从 moreDiscountMap 中获取相同用券组合下的当前最优方案
            CouponDiscountDTO best = moreDiscountMap.get(ids);
            // 如果已经存在最优方案，且其优惠金额大于等于当前方案的优惠金额
            if (best != null && best.getDiscountAmount() >= solution.getDiscountAmount()) {
                // 当前方案优惠金额少，跳过该方案，继续处理下一个方案
                continue;
            }
            // 2.3.比较金额相同时，用券数量是否最少
            // 从 lessCouponMap 中获取相同优惠金额下的当前最优方案
            best = lessCouponMap.get(solution.getDiscountAmount());
            // 获取当前方案使用的优惠券数量
            int size = solution.getIds().size();
            // 如果当前方案使用的优惠券数量大于1，且已经存在最优方案，且其用券数量小于等于当前方案的用券数量
            if (size > 1 && best != null && best.getIds().size() <= size) {
                // 当前方案用券更多，放弃该方案，继续处理下一个方案
                continue;
            }
            // 2.4.更新最优解
            // 如果当前方案在上述比较中更优，则更新 moreDiscountMap 和 lessCouponMap
            moreDiscountMap.put(ids, solution);
            lessCouponMap.put(solution.getDiscountAmount(), solution);
        }
        // 3.求交集
        // 找出同时满足用券相同时优惠金额最大，且金额相同时用券数量最少的方案
        Collection<CouponDiscountDTO> bestSolutions = CollUtils
                .intersection(moreDiscountMap.values(), lessCouponMap.values());
        // 4.排序，按优惠金额降序
        // 对筛选出的最优方案按优惠金额降序排序，并将结果转换为列表返回
        return bestSolutions.stream()
                .sorted(Comparator.comparingInt(CouponDiscountDTO::getDiscountAmount).reversed())
                .collect(Collectors.toList());
    }

    // ______________________________________________________________________________________________________________

    /**
     * 根据券方案计算订单优惠明细
     * @param orderCouponDTO
     * @return
     */
    @Override
    public CouponDiscountDTO queryDiscountDetailByOrder(OrderCouponDTO orderCouponDTO) {
        // 1.查询用户优惠券
        List<Long> userCouponIds = orderCouponDTO.getUserCouponIds();
        List<Coupon> coupons = userCouponMapper.queryCouponByUserCouponIds(userCouponIds, UserCouponStatus.UNUSED);
        if (CollUtils.isEmpty(coupons)) {
            return null;
        }
        // 2.查询优惠券对应课程
        Map<Coupon, List<OrderCourseDTO>> availableCouponMap = findAvailableCoupon(coupons, orderCouponDTO.getCourseList());
        if (CollUtils.isEmpty(availableCouponMap)) {
            return null;
        }
        // 3.查询优惠券规则
        return calculateSolutionDiscount(availableCouponMap, orderCouponDTO.getCourseList(), coupons);
    }

}




